
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: MethodGeneratorSupport.java 377 2010-03-11 20:50:18Z archie.cobbs $
 */

package org.dellroad.sidekar.classgen;

import static org.dellroad.sidekar.asm.Opcodes.ACC_PRIVATE;
import static org.dellroad.sidekar.asm.Opcodes.ACC_PROTECTED;
import static org.dellroad.sidekar.asm.Opcodes.ACC_PUBLIC;
import static org.dellroad.sidekar.asm.Opcodes.ALOAD;
import static org.dellroad.sidekar.asm.Opcodes.GETFIELD;
import static org.dellroad.sidekar.asm.Opcodes.ILOAD;
import static org.dellroad.sidekar.asm.Opcodes.INVOKESTATIC;
import static org.dellroad.sidekar.asm.Opcodes.INVOKEVIRTUAL;
import static org.dellroad.sidekar.asm.Opcodes.IRETURN;

import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;

import org.dellroad.sidekar.Entity;
import org.dellroad.sidekar.Item;
import org.dellroad.sidekar.SimpleField;
import org.dellroad.sidekar.asm.ClassWriter;
import org.dellroad.sidekar.asm.MethodVisitor;
import org.dellroad.sidekar.asm.Type;
import org.dellroad.sidekar.schema.model.FieldModel;
import org.dellroad.stuff.java.Primitive;

/**
 * Support for generating model object class methods.
 */
abstract class MethodGeneratorSupport {

    static final String ID_FIELD = "$id";
    static final String ENTITY_FIELD = "$entity";

    final String className;
    final Method getMethod;
    final Method setMethod;
    final Method getSidekarIdMethod;
    final Method getSidekarEntityMethod;
    final Method attachMethod;
    final Method detachMethod;
    final Method isAttachedMethod;
    final Method isSidekarAttachedMethod;
    final Method validateMethod;
    final Method sidekarAttachMethod;
    final Method sidekarDetachMethod;
    final Method sidekarValidateMethod;
    final Method getFieldMethod;

    MethodGeneratorSupport(String className) {
        this.className = className;
        try {
            this.getMethod = SimpleField.class.getMethod("get", Item.class);
            this.setMethod = SimpleField.class.getMethod("set", Item.class, Object.class);
            this.getSidekarEntityMethod = Item.class.getMethod("getSidekarEntity");
            this.getSidekarIdMethod = Item.class.getMethod("getSidekarId");
            this.sidekarAttachMethod = Item.class.getMethod("sidekarAttach");
            this.sidekarDetachMethod = Item.class.getMethod("sidekarDetach");
            this.sidekarValidateMethod = Item.class.getMethod("sidekarValidate");
            this.isSidekarAttachedMethod = Item.class.getMethod("isSidekarAttached");
            this.attachMethod = Entity.class.getMethod("attach", Item.class);
            this.detachMethod = Entity.class.getMethod("detach", Item.class);
            this.isAttachedMethod = Entity.class.getMethod("isAttached", Item.class);
            this.validateMethod = Entity.class.getMethod("validate", Item.class);
            this.getFieldMethod = Entity.class.getMethod("getField", String.class);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("internal error", e);
        }
    }

    /**
     * Emit a method that forwards a method call. Within the generated method,
     * this invokes the {@link CodeEmitter} to push the 'this' instance and any additional preceeding
     * parameters, then copies the method's parameters and invokes the method.
     *
     * <p>
     * It is assumed that "to" parameters are all objects. Wrapper instances are automatically
     * created for primitive "from" parameters.
     * </p>
     */
    void forwardMethod(ClassWriter cw, Method from, Method to, CodeEmitter pre, CodeEmitter post) {

        // If no "from" method, nothing to do
        if (from == null)
            return;

        // Generate new "from" method
        MethodVisitor mv = cw.visitMethod(copyAccess(from), from.getName(),
          Type.getMethodDescriptor(from), null, getExceptionNames(from));

        // Do prep work
        if (pre != null)
            pre.write(mv);

        // Copy "from" method parameters
        Class<?>[] ptypes = from.getParameterTypes();
        int varnum = 1;
        for (int i = 0; i < ptypes.length; i++) {

            // Get "from" parameter type
            Class<?> jtype = ptypes[i];
            Type ptype = Type.getType(jtype);

            // Wrap primitive types
            if (jtype.isPrimitive()) {
                mv.visitVarInsn(ptype.getOpcode(ILOAD), varnum);
                Type wrapperType = Type.getType(Primitive.get(jtype).getWrapperType());
                mv.visitMethodInsn(INVOKESTATIC, wrapperType.getInternalName(), "valueOf",
                  Type.getMethodDescriptor(wrapperType, new Type[] { Type.getType(jtype) }));
            } else
                mv.visitVarInsn(ALOAD, varnum);

            // Advance local variable pointer
            varnum += ptype.getSize();
        }

        // Invoke "to" method
        emitInvoke(mv, to);

        // Return what it returned
        if (post != null)
            post.write(mv);
        mv.visitInsn(Type.getType(from.getReturnType()).getOpcode(IRETURN));

        // Finish up
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    /**
     * Emit code to invoke a method. This assumes the stack is loaded.
     */
    void emitInvoke(MethodVisitor mv, Method method) {
        mv.visitMethodInsn((method.getModifiers() & Modifier.STATIC) != 0 ? INVOKESTATIC : INVOKEVIRTUAL,
          Type.getInternalName(method.getDeclaringClass()), method.getName(), Type.getMethodDescriptor(method));
    }

    /**
     * Emit an accessor method implementation.
     */
    void emitAccessor(ClassWriter cw, Method method, String fieldName) {
        Type returnType = Type.getType(method.getReturnType());
        MethodVisitor mv = cw.visitMethod(copyAccess(method), method.getName(), Type.getMethodDescriptor(method), null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, this.className, fieldName, returnType.getDescriptor());
        mv.visitInsn(returnType.getOpcode(IRETURN));
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    /**
     * Push the item's entity onto the stack.
     *
     * {@code ... -&gt; ..., this.$entity}
     */
    void getEntity(MethodVisitor mv) {
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, this.className, ENTITY_FIELD, Type.getType(Entity.class).getDescriptor());
    }

    /**
     * Push a field onto the stack.
     *
     * {@code ... -&gt; ..., this.xxx_field}
     */
    void getField(MethodVisitor mv, FieldModel fieldModel) {
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, this.className,
          ClassGenerator.getGenFieldName(fieldModel), ClassGenerator.getGenFieldType(fieldModel).getDescriptor());
    }

    String[] getExceptionNames(Method method) {
        ArrayList<String> list = new ArrayList<String>();
        for (Class<?> type : method.getExceptionTypes())
            list.add(Type.getType(type).getInternalName());
        return list.toArray(new String[list.size()]);
    }

    int copyAccess(Member member) {
        return member.getModifiers() & (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED);
    }
}

